layout.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import { Inter, Permanent_Marker } from "next/font/google";
  2. import { GeistSans } from "geist/font/sans";
  3. import { GeistMono } from "geist/font/mono";
  4. import { cn } from "@/shared/lib/utils";
  5. import { generateStructuredData, StructuredDataScript } from "@/shared/lib/structured-data";
  6. import { getServerUrl } from "@/shared/lib/server-url";
  7. import { SiteConfig } from "@/shared/config/site-config";
  8. import { WorkoutSessionsSynchronizer } from "@/features/workout-session/ui/workout-sessions-synchronizer";
  9. import { ThemeSynchronizer } from "@/features/theme/ui/ThemeSynchronizer";
  10. import { Header } from "@/features/layout/Header";
  11. import { Footer } from "@/features/layout/Footer";
  12. import { Version } from "@/components/version";
  13. import { TailwindIndicator } from "@/components/utils/TailwindIndicator";
  14. import { NextTopLoader } from "@/components/ui/next-top-loader";
  15. import { ServiceWorkerRegistration } from "@/components/pwa/ServiceWorkerRegistration";
  16. import { Providers } from "./providers";
  17. import type { ReactElement } from "react";
  18. import type { Metadata } from "next";
  19. import "@/shared/styles/globals.css";
  20. export const metadata: Metadata = {
  21. title: {
  22. default: SiteConfig.title,
  23. template: `%s | ${SiteConfig.title}`,
  24. },
  25. description: SiteConfig.description,
  26. keywords: SiteConfig.keywords,
  27. applicationName: SiteConfig.seo.applicationName,
  28. category: SiteConfig.seo.category,
  29. classification: SiteConfig.seo.classification,
  30. metadataBase: new URL(getServerUrl()),
  31. manifest: "/manifest.json",
  32. robots: {
  33. index: true,
  34. follow: true,
  35. googleBot: {
  36. index: true,
  37. follow: true,
  38. "max-snippet": -1,
  39. "max-image-preview": "large",
  40. "max-video-preview": -1,
  41. },
  42. },
  43. verification: {
  44. google: process.env.GOOGLE_SITE_VERIFICATION,
  45. },
  46. openGraph: {
  47. title: SiteConfig.title,
  48. description: SiteConfig.description,
  49. url: getServerUrl(),
  50. siteName: SiteConfig.title,
  51. images: [
  52. {
  53. url: `${getServerUrl()}/images/default-og-image_fr.jpg`,
  54. width: SiteConfig.seo.ogImage.width,
  55. height: SiteConfig.seo.ogImage.height,
  56. alt: `${SiteConfig.title} - Plateforme de fitness moderne`,
  57. },
  58. {
  59. url: `${getServerUrl()}/images/default-og-image_en.jpg`,
  60. width: SiteConfig.seo.ogImage.width,
  61. height: SiteConfig.seo.ogImage.height,
  62. alt: `${SiteConfig.title} - Modern fitness platform`,
  63. },
  64. ],
  65. locale: "fr_FR",
  66. type: "website",
  67. },
  68. twitter: {
  69. card: "summary_large_image",
  70. site: SiteConfig.seo.twitterHandle,
  71. creator: SiteConfig.seo.twitterHandle,
  72. title: SiteConfig.title,
  73. description: SiteConfig.description,
  74. images: [
  75. {
  76. url: `${getServerUrl()}/images/default-og-image_fr.jpg`,
  77. width: SiteConfig.seo.ogImage.width,
  78. height: SiteConfig.seo.ogImage.height,
  79. alt: `${SiteConfig.title} - Plateforme de fitness moderne`,
  80. },
  81. ],
  82. },
  83. alternates: {
  84. canonical: "https://www.workout.cool",
  85. languages: {
  86. "fr-FR": "https://www.workout.cool/fr",
  87. "en-US": "https://www.workout.cool/en",
  88. "x-default": "https://www.workout.cool",
  89. },
  90. },
  91. authors: [{ name: SiteConfig.company.name, url: getServerUrl() }],
  92. creator: SiteConfig.company.name,
  93. publisher: SiteConfig.company.name,
  94. formatDetection: {
  95. email: false,
  96. address: false,
  97. telephone: false,
  98. },
  99. appleWebApp: {
  100. capable: true,
  101. statusBarStyle: "default",
  102. title: SiteConfig.title,
  103. },
  104. icons: {
  105. icon: [
  106. { url: "/images/favicon-32x32.png", sizes: "32x32", type: "image/png" },
  107. { url: "/images/favicon-16x16.png", sizes: "16x16", type: "image/png" },
  108. { url: "/images/favicon.ico", type: "image/x-icon" },
  109. ],
  110. apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
  111. shortcut: "/images/favicon.ico",
  112. },
  113. other: {
  114. "msapplication-TileColor": "#FF5722",
  115. "msapplication-TileImage": "/android-chrome-192x192.png",
  116. },
  117. };
  118. const inter = Inter({
  119. subsets: ["latin"],
  120. variable: "--font-inter",
  121. display: "swap",
  122. });
  123. const permanentMarker = Permanent_Marker({
  124. weight: "400",
  125. subsets: ["latin"],
  126. variable: "--font-permanent-marker",
  127. display: "swap",
  128. });
  129. export const preferredRegion = ["fra1", "sfo1", "iad1"];
  130. interface RootLayoutProps {
  131. params: Promise<{ locale: string }>;
  132. children: ReactElement;
  133. }
  134. export default async function RootLayout({ params, children }: RootLayoutProps) {
  135. const { locale } = await params;
  136. // Generate structured data
  137. const websiteStructuredData = generateStructuredData({
  138. type: "WebSite",
  139. locale,
  140. });
  141. const organizationStructuredData = generateStructuredData({
  142. type: "Organization",
  143. locale,
  144. });
  145. const webAppStructuredData = generateStructuredData({
  146. type: "WebApplication",
  147. locale,
  148. });
  149. return (
  150. <>
  151. <html className="h-full" dir="ltr" lang={locale} suppressHydrationWarning>
  152. <head>
  153. <meta charSet="UTF-8" />
  154. <meta content="width=device-width, initial-scale=1, maximum-scale=1 viewport-fit=cover" name="viewport" />
  155. {/* PWA Meta Tags */}
  156. <meta content="yes" name="apple-mobile-web-app-capable" />
  157. <meta content="default" name="apple-mobile-web-app-status-bar-style" />
  158. <meta content="Workout Cool" name="apple-mobile-web-app-title" />
  159. <meta content="yes" name="mobile-web-app-capable" />
  160. <meta content="#FF5722" name="msapplication-TileColor" />
  161. <meta content="/android-chrome-192x192.png" name="msapplication-TileImage" />
  162. {/* PWA Manifest */}
  163. <link href="/manifest.json" rel="manifest" />
  164. {/* eslint-disable-next-line @next/next/no-page-custom-font */}
  165. <link as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="preload" />
  166. {/* Alternate hreflang for i18n */}
  167. <link href="https://www.workout.cool/fr" hrefLang="fr" rel="alternate" />
  168. <link href="https://www.workout.cool/en" hrefLang="en" rel="alternate" />
  169. {/* Theme color for PWA */}
  170. <meta content="#FF5722" name="theme-color" />
  171. {/* Structured Data */}
  172. <StructuredDataScript data={websiteStructuredData} />
  173. <StructuredDataScript data={organizationStructuredData} />
  174. <StructuredDataScript data={webAppStructuredData} />
  175. </head>
  176. <body
  177. className={cn(
  178. "flex items-center justify-center min-h-screen w-full p-8 max-sm:p-0 max-sm:min-h-full bg-base-200 dark:bg-[#18181b] dark:text-gray-200 antialiased",
  179. "bg-hero-light dark:bg-hero-dark",
  180. GeistMono.variable,
  181. GeistSans.variable,
  182. inter.variable,
  183. permanentMarker.variable,
  184. )}
  185. suppressHydrationWarning
  186. >
  187. <Providers locale={locale}>
  188. <ServiceWorkerRegistration />
  189. <WorkoutSessionsSynchronizer />
  190. <ThemeSynchronizer />
  191. <NextTopLoader color="#FF5722" delay={100} showSpinner={false} />
  192. {/* Main Card Container */}
  193. <div className="card w-full max-w-3xl min-h-[500px] max-h-[90vh] h-[80vh] bg-white dark:bg-[#232324] shadow-xl border border-base-200 dark:border-slate-700 flex flex-col justify-between overflow-hidden max-sm:rounded-none max-sm:h-full rounded-lg">
  194. <Header />
  195. <div className="flex-1 overflow-auto flex flex-col">{children}</div>
  196. <Footer />
  197. </div>
  198. <Version />
  199. <TailwindIndicator />
  200. </Providers>
  201. </body>
  202. </html>
  203. </>
  204. );
  205. }